<template>
  <div :class="classes">
    <Input-number
      v-if="!range && showInput"
      :min="min"
      :size="inputSize"
      :max="max"
      :step="step"
      :value="exportValue[0]"
      :disabled="disabled"
      :active-change="activeChange"
      @on-change="handleInputChange"
    ></Input-number>
    <div :class="[prefixCls + '-wrap']" ref="slider" @click.self="sliderClick">
      <input type="hidden" :name="name" :value="exportValue">
      <template v-if="showStops">
        <div
          :class="[prefixCls + '-stop']"
          v-for="item in stops"
          :style="{ 'left': item + '%' }"
          @click.self="sliderClick"
        ></div>
      </template>
      <div :class="[prefixCls + '-bar']" :style="barStyle" @click.self="sliderClick"></div>
      <div
        :class="[prefixCls + '-button-wrap']"
        :style="{left: minPosition + '%'}"
        @touchstart="onPointerDown($event, 'min')"
        @mousedown="onPointerDown($event, 'min')"
      >
        <Tooltip
          :controlled="pointerDown === 'min'"
          placement="top"
          :content="tipFormat(exportValue[0])"
          :disabled="tipDisabled"
          :always="showTip === 'always'"
          ref="minTooltip"
        >
          <div
            :class="minButtonClasses"
            tabindex="0"
            @focus="handleFocus('min')"
            @blur="handleBlur('min')"
            @keydown.left="onKeyLeft($event, 'min')"
            @keydown.down="onKeyLeft($event, 'min')"
            @keydown.right="onKeyRight($event, 'min')"
            @keydown.up="onKeyRight($event, 'min')"
          ></div>
        </Tooltip>
      </div>
      <div
        v-if="range"
        :class="[prefixCls + '-button-wrap']"
        :style="{left: maxPosition + '%'}"
        @touchstart="onPointerDown($event, 'max')"
        @mousedown="onPointerDown($event, 'max')"
      >
        <Tooltip
          :controlled="pointerDown === 'max'"
          placement="top"
          :content="tipFormat(exportValue[1])"
          :disabled="tipDisabled"
          :always="showTip === 'always'"
          ref="maxTooltip"
        >
          <div
            :class="maxButtonClasses"
            tabindex="0"
            @focus="handleFocus('max')"
            @blur="handleBlur('max')"
            @keydown.left="onKeyLeft($event, 'max')"
            @keydown.down="onKeyLeft($event, 'max')"
            @keydown.right="onKeyRight($event, 'max')"
            @keydown.up="onKeyRight($event, 'max')"
          ></div>
        </Tooltip>
      </div>
    </div>
  </div>
</template>
<script>
import InputNumber from "./input-number.vue";
import Tooltip from "./tooltip/tooltip.vue";
import { getStyle, oneOf } from "./utils/assist";
import { on, off } from "./utils/dom";
import Emitter from "./mixins/emitter";
import elementResizeDetectorMaker from "element-resize-detector";

const prefixCls = "xbsj-slider";

export default {
  name: "Slider",
  mixins: [Emitter],
  components: { InputNumber, Tooltip },
  props: {
    min: {
      type: Number,
      default: 0
    },
    max: {
      type: Number,
      default: 100
    },
    step: {
      type: Number,
      default: 1
    },
    range: {
      type: Boolean,
      default: false
    },
    value: {
      type: [Number, Array],
      default: 0
    },
    disabled: {
      type: Boolean,
      default: false
    },
    showInput: {
      type: Boolean,
      default: false
    },
    inputSize: {
      type: String,
      default: "default",
      validator(value) {
        return oneOf(value, ["small", "large", "default"]);
      }
    },
    showStops: {
      type: Boolean,
      default: false
    },
    tipFormat: {
      type: Function,
      default(val) {
        return val;
      }
    },
    showTip: {
      type: String,
      default: "hover",
      validator(value) {
        return oneOf(value, ["hover", "always", "never"]);
      }
    },
    name: {
      type: String
    },
    // 3.4.0
    activeChange: {
      type: Boolean,
      default: true
    }
  },
  data() {
    const val = this.checkLimits(
      Array.isArray(this.value) ? this.value : [this.value]
    );
    return {
      prefixCls: prefixCls,
      currentValue: val,
      dragging: false,
      pointerDown: "",
      startX: 0,
      currentX: 0,
      startPos: 0,
      oldValue: [...val],
      valueIndex: {
        min: 0,
        max: 1
      },
      sliderWidth: 0
    };
  },
  watch: {
    value(val) {
      val = this.checkLimits(Array.isArray(val) ? val : [val]);
      if (
        !this.dragging &&
        (val[0] !== this.currentValue[0] || val[1] !== this.currentValue[1])
      ) {
        this.currentValue = val;
      }
    },
    exportValue(values) {
      this.$nextTick(() => {
        this.$refs.minTooltip.updatePopper();
        if (this.range) {
          this.$refs.maxTooltip.updatePopper();
        }
      });
      const value = this.range ? values : values[0];
      this.$emit("input", value);
      this.$emit("on-input", value);
    }
  },
  computed: {
    classes() {
      return [
        `${prefixCls}`,
        {
          [`${prefixCls}-input`]: this.showInput && !this.range,
          [`${prefixCls}-range`]: this.range,
          [`${prefixCls}-disabled`]: this.disabled
        }
      ];
    },
    minButtonClasses() {
      return [
        `${prefixCls}-button`,
        {
          [`${prefixCls}-button-dragging`]: this.pointerDown === "min"
        }
      ];
    },
    maxButtonClasses() {
      return [
        `${prefixCls}-button`,
        {
          [`${prefixCls}-button-dragging`]: this.pointerDown === "max"
        }
      ];
    },
    exportValue() {
      const decimalCases = (String(this.step).split(".")[1] || "").length;
      return this.currentValue.map(nr => Number(nr.toFixed(decimalCases)));
    },
    minPosition() {
      const val = this.currentValue;
      return ((val[0] - this.min) / this.valueRange) * 100;
    },
    maxPosition: function() {
      const val = this.currentValue;

      return ((val[1] - this.min) / this.valueRange) * 100;
    },
    barStyle() {
      const style = {
        width: ((this.currentValue[0] - this.min) / this.valueRange) * 100 + "%"
      };

      if (this.range) {
        style.left =
          ((this.currentValue[0] - this.min) / this.valueRange) * 100 + "%";
        style.width =
          ((this.currentValue[1] - this.currentValue[0]) / this.valueRange) *
            100 +
          "%";
      }

      return style;
    },
    stops() {
      let stopCount = this.valueRange / this.step;
      let result = [];
      let stepWidth = (100 * this.step) / this.valueRange;
      for (let i = 1; i < stopCount; i++) {
        result.push(i * stepWidth);
      }
      return result;
    },
    tipDisabled() {
      return (
        this.tipFormat(this.currentValue[0]) === null ||
        this.showTip === "never"
      );
    },
    valueRange() {
      return this.max - this.min;
    },
    firstPosition() {
      return this.currentValue[0];
    },
    secondPosition() {
      return this.currentValue[1];
    }
  },
  methods: {
    getPointerX(e) {
      return e.type.indexOf("touch") !== -1 ? e.touches[0].clientX : e.clientX;
    },
    checkLimits([min, max]) {
      min = Math.max(this.min, min);
      min = Math.min(this.max, min);

      max = Math.max(this.min, min, max);
      max = Math.min(this.max, max);
      return [min, max];
    },
    getCurrentValue(event, type) {
      if (this.disabled) {
        return;
      }

      const index = this.valueIndex[type];
      if (typeof index === "undefined") {
        return;
      }

      return this.currentValue[index];
    },
    onKeyLeft(event, type) {
      const value = this.getCurrentValue(event, type);
      if (Number.isFinite(value)) {
        this.changeButtonPosition(value - this.step, type);
      }
    },
    onKeyRight(event, type) {
      const value = this.getCurrentValue(event, type);
      if (Number.isFinite(value)) {
        this.changeButtonPosition(value + this.step, type);
      }
    },
    onPointerDown(event, type) {
      if (this.disabled) return;
      event.preventDefault();
      this.pointerDown = type;

      this.onPointerDragStart(event);
      on(window, "mousemove", this.onPointerDrag);
      on(window, "touchmove", this.onPointerDrag);
      on(window, "mouseup", this.onPointerDragEnd);
      on(window, "touchend", this.onPointerDragEnd);
    },
    onPointerDragStart(event) {
      this.dragging = false;
      this.startX = this.getPointerX(event);
      this.startPos =
        (this[`${this.pointerDown}Position`] * this.valueRange) / 100 +
        this.min;
    },
    onPointerDrag(event) {
      this.dragging = true;
      this.$refs[`${this.pointerDown}Tooltip`].visible = true;
      this.currentX = this.getPointerX(event);
      const diff =
        ((this.currentX - this.startX) / this.sliderWidth) * this.valueRange;

      this.changeButtonPosition(this.startPos + diff);
    },
    onPointerDragEnd() {
      if (this.dragging) {
        this.dragging = false;
        this.$refs[`${this.pointerDown}Tooltip`].visible = false;
        this.emitChange();
      }

      this.pointerDown = "";
      off(window, "mousemove", this.onPointerDrag);
      off(window, "touchmove", this.onPointerDrag);
      off(window, "mouseup", this.onPointerDragEnd);
      off(window, "touchend", this.onPointerDragEnd);
    },
    changeButtonPosition(newPos, forceType) {
      const type = forceType || this.pointerDown;
      const index = type === "min" ? 0 : 1;
      if (type === "min") newPos = this.checkLimits([newPos, this.max])[0];
      else newPos = this.checkLimits([this.min, newPos])[1];

      const modulus = this.handleDecimal(newPos, this.step);
      const value = this.currentValue;
      value[index] = newPos - modulus;

      // 判断左右是否相等，否则会出现左边大于右边的情况
      if (this.range) {
        if (type === "min" && value[0] > value[1]) value[1] = value[0];
        if (type === "max" && value[0] > value[1]) value[0] = value[1];
      }

      this.currentValue = [...value];

      if (!this.dragging) {
        if (this.currentValue[index] !== this.oldValue[index]) {
          this.emitChange();
          this.oldValue[index] = this.currentValue[index];
        }
      }
    },
    handleDecimal(pos, step) {
      if (step < 1) {
        let sl = step.toString(),
          multiple = 1,
          m;
        try {
          m = sl.split(".")[1].length;
        } catch (e) {
          m = 0;
        }
        multiple = Math.pow(10, m);
        return ((pos * multiple) % (step * multiple)) / multiple;
      } else return pos % step;
    },
    emitChange() {
      const value = this.range ? this.exportValue : this.exportValue[0];
      this.$emit("on-change", value);
      this.dispatch("FormItem", "on-form-change", value);
    },

    sliderClick(event) {
      if (this.disabled) return;
      const currentX = this.getPointerX(event);
      const sliderOffsetLeft = this.$refs.slider.getBoundingClientRect().left;
      let newPos =
        ((currentX - sliderOffsetLeft) / this.sliderWidth) * this.valueRange +
        this.min;
      let regularNewPos = (newPos / this.valueRange) * 100;

      if (!this.range || regularNewPos <= this.minPosition)
        this.changeButtonPosition(newPos, "min");
      else if (regularNewPos >= this.maxPosition)
        this.changeButtonPosition(newPos, "max");
      else
        this.changeButtonPosition(
          newPos,
          newPos - this.firstPosition <= this.secondPosition - newPos
            ? "min"
            : "max"
        );
    },

    handleInputChange(val) {
      this.currentValue = [
        val === 0 ? 0 : val || this.min,
        this.currentValue[1]
      ];
      this.emitChange();
    },

    handleFocus(type) {
      this.$refs[`${type}Tooltip`].handleShowPopper();
    },

    handleBlur(type) {
      this.$refs[`${type}Tooltip`].handleClosePopper();
    },
    handleSetSliderWidth() {
      this.sliderWidth = parseInt(getStyle(this.$refs.slider, "width"), 10);
    }
  },
  mounted() {
    /*
    // #2852
    this.$on("on-visible-change", val => {
      if (val && this.showTip === "always") {
        this.$refs.minTooltip.doDestroy();
        if (this.range) {
          this.$refs.maxTooltip.doDestroy();
        }
        this.$nextTick(() => {
          this.$refs.minTooltip.updatePopper();
          if (this.range) {
            this.$refs.maxTooltip.updatePopper();
          }
        });
      } 
    });
    */
    this.observer = elementResizeDetectorMaker();
    this.observer.listenTo(this.$refs.slider, this.handleSetSliderWidth);

  },
  beforeDestroy() {
    this.observer.removeListener(this.$refs.slider, this.handleSetSliderWidth);
  }
};
</script>
